﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Collections.Generic;
using Tesla.Bounding;
using Tesla.Content;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Math;

namespace Tesla.Scene {
    /// <summary>
    /// A Mesh is a "leaf" element in the scene graph that is actual geometry that can be rendered by the device. It contains a material
    /// that describes how it is to be rendered, a model bound, and a final light collection that is sorted.
    /// </summary>
    public class Mesh : Spatial, IRenderable, IPickable {
        private MeshData _meshData;
        private Material _material;
        private BoundingVolume _modelBound;
        private LightCollection _worldLights;

        /// <summary>
        /// Gets if the mesh is pickable. Same as querying the scene hint's picking hint.
        /// </summary>
        public bool IsPickable {
            get {
                PickingHint hint = base.SceneHints.PickingHint;
                return (hint == PickingHint.PickableAndCollidable || hint == PickingHint.Pickable);
            }
            set {
                SceneHints hints = base.SceneHints;
                PickingHint hint = hints.PickingHint;
                if(value) {
                    if(hint == PickingHint.Collidable) {
                        hints.PickingHint = PickingHint.PickableAndCollidable;
                    } else if(hint == PickingHint.None || hint == PickingHint.Inherit) {
                        hints.PickingHint = PickingHint.Pickable;
                    } 
                } else {
                    if(hint == PickingHint.PickableAndCollidable) {
                        hints.PickingHint = PickingHint.Collidable;
                    } else if(hint == PickingHint.Pickable || hint == PickingHint.Inherit) {
                        hints.PickingHint = PickingHint.None;
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the MeshData, representing the actual geometry of the mesh.
        /// </summary>
        public MeshData MeshData {
            get {
                return _meshData;
            }
            set {
                _meshData = value;
            }
        }

        /// <summary>
        /// Gets or sets the Material, which describes how to render the mesh.
        /// </summary>
        public Material Material {
            get {
                return _material;
            }
            set {
                _material = value;
                _worldLights.RefreshShader = true;
            }
        }

        /// <summary>
        /// Gets the ortho order. When the renderable is used in the ortho render bucket,
        /// it is sorted based on this value (lower values are rendered first and higher values
        /// last).
        /// </summary>
        public int OrthoOrder {
            get {
                return base.SceneHints.OrthoOrder;
            }
            set {
                base.SceneHints.OrthoOrder = value;
            }
        }

        /// <summary>
        /// Gets or sets the renderable's render bucket type. 
        /// </summary>
        public RenderBucketType RenderBucketType {
            get {
                return base.SceneHints.RenderBucketType;
            }
            set {
                base.SceneHints.RenderBucketType = value;
            }
        }

        /// <summary>
        /// Gets the transparency mode to use, one-pass or two-pass.
        /// </summary>
        public TransparencyType TransparencyType {
            get {
                return base.SceneHints.TransparencyType;
            }
            set {
                base.SceneHints.TransparencyType = value;
            }
        }

        /// <summary>
        /// Gets or sets the mesh's bounding volume, which contains the entire geometry in model (local) space.
        /// </summary>
        public BoundingVolume ModelBound {
            get {
                return _modelBound;
            }
            set {
                SetModelBound(value);
            }
        }

        /// <summary>
        /// Gets the world light collection owned by this Mesh.
        /// </summary>
        public ILightCollection WorldLights {
            get {
                return _worldLights;
            }
        }

        /// <summary>
        /// Do not use directly, for ISavable
        /// </summary>
        protected Mesh() {}

        /// <summary>
        /// Creates a new Mesh.
        /// </summary>
        /// <param name="name">Mesh name</param>
        public Mesh(String name) 
            : base(name) {
            _meshData = new MeshData();
            _worldLights = new LightCollection();
        }

        /// <summary>
        /// Creates a new Mesh using the supplied MeshData object, which may or may not be null. Useful
        /// if multiple meshes are to share the same geometry.
        /// </summary>
        /// <param name="name">Mesh name.</param>
        /// <param name="meshData">Mesh data, may be null</param>
        public Mesh(String name, MeshData meshData)
            :base(name) {
                _meshData = meshData;
                _worldLights = new LightCollection();
        }

        /// <summary>
        /// Draws the spatial if it skips the render queue, or adds this mesh
        /// to the queue to be rendered at a later time. 
        /// </summary>
        /// <param name="renderer">Renderer</param>
        public override void Draw(IRenderer renderer) {
            //Invalid materials should not be drawn at all
            if(_material == null || !_material.IsValid) {
                return;
            }

            if(renderer.RenderQueue.AddToQueue(this)) {
                return;
            }
            //Otherwise, render immediately
            Render(renderer);
        }

        /// <summary>
        /// Makes the actual device calls to render the mesh.
        /// </summary>
        /// <param name="renderer">Renderer</param>
        public virtual void Render(IRenderer renderer) {
            //Get the renderer's enforced material
            Material enforcedMaterial = renderer.EnforcedMaterial;

            //If that is null, use our material
            if(enforcedMaterial == null) {
                enforcedMaterial = _material;
            }

            //If the material to be used is null or not valid, drop out
            if(enforcedMaterial == null || !enforcedMaterial.IsValid) {
                return;
            }

            //Return if we have no mesh data to render
            if(_meshData == null) {
                return;
            }

            //Are we using indices?
            bool useIndices = _meshData.UseIndexedPrimitives;

            //Are our buffers valid?
            if((_meshData.VertexBuffer == null || _meshData.VertexBuffer.IsDisposed) 
                || (useIndices && (_meshData.IndexBuffer == null || _meshData.IndexBuffer.IsDisposed))) {
                return;
            }

            //Apply the material (flush engine values, set render state data, execute any pre-shader logic)
            enforcedMaterial.ApplyMaterial(renderer, this);

            //Set the buffers
            renderer.SetVertexBuffer(_meshData.VertexBuffer);
            if(useIndices) {
                renderer.SetIndexBuffer(_meshData.IndexBuffer);
            }

            //For each pass
            for(int i = 0; i < enforcedMaterial.PassCount; i++) {
                //Apply the pass
                enforcedMaterial.ApplyPass(renderer, i);

                //Draw
                if(useIndices) {
                    renderer.DrawIndexed(_meshData.PrimitiveType, _meshData.IndexCount, 0, 0);
                } else {
                    renderer.Draw(_meshData.PrimitiveType, _meshData.VertexCount, 0);
                }
            }
        }

        /// <summary>
        /// Sets the render state onto the Material, if it exists. If not, the state is lost.
        /// </summary>
        /// <param name="rs">Renderstate</param>
        public override void SetRenderState(RenderState rs) {
            if(_material != null) {
                _material.SetRenderState(rs);
            }
        }

        /// <summary>
        /// Updates the world bounding of this Mesh, which is simply the model bound transformed by the world transformation
        /// of the mesh.
        /// </summary>
        /// <param name="recurse">Unused</param>
        public override void UpdateWorldBound(bool recurse) {
            if(_modelBound != null) {
                //Set the world bounding to the model bound and transform it to world space. As long as
                //model bound is not null, world bound is guaranteed to have been created/same type
                base.WorldBounding.Set(_modelBound);
                base.WorldBounding.TransformLocal(base.WorldTransform);
            } else {
                base.WorldBounding = null;
            }
            ClearDirty(DirtyMark.Bounding);
        }

        /// <summary>
        /// Set the model bound. Same as using mesh.ModelBound property.
        /// </summary>
        /// <param name="volume">Volume to set</param>
        public override void SetModelBound(BoundingVolume volume) {
            _modelBound = volume;
            if(volume != null) {
                base.WorldBounding = volume.Clone();
            } else {
                base.WorldBounding = null;
            }
            UpdateModelBound();
        }

        /// <summary>
        /// Update the model bound based on the mesh's geometric data.
        /// </summary>
        public void UpdateModelBound() {
            if(_meshData == null) {
                return;
            }
            if(_modelBound != null) {
                
                if(_meshData.UseIndexedPrimitives) {
                    if(_meshData.Indices.ElementType == typeof(int)) {
                        DataBuffer<Vector3> pos = _meshData.Positions;
                        DataBuffer<int> ind = (DataBuffer<int>) _meshData.Indices;
                        if(pos != null && ind != null) {
                        _modelBound.ComputeFromPrimitives(pos, ind);
                        }
                    } else {
                        DataBuffer<Vector3> pos = _meshData.Positions;
                        DataBuffer<short> ind = (DataBuffer<short>) _meshData.Indices;
                        if(pos != null && ind != null) {
                            _modelBound.ComputeFromPrimitives(pos, ind);
                        }
                    }
                } else {
                    DataBuffer<Vector3> pos = _meshData.Positions;
                    if(pos != null) {
                        _modelBound.ComputeFromPoints(pos);
                    }
                }
                PropagateDirtyUp(DirtyMark.Bounding);
            }
        }

        /// <summary>
        /// Set the mesh's material. Same as mesh.Material property
        /// </summary>
        /// <param name="material">Material to set</param>
        public override void SetMaterial(Material material) {
            _material = material;
            _worldLights.RefreshShader = true;
        }

        /// <summary>
        /// Sorts the mesh's world lights.
        /// </summary>
        public override void SortLights() {
            _worldLights.SortLights(this);
        }

        /// <summary>
        /// Updates world lights.
        /// </summary>
        /// <param name="recurse">True if children should be updated</param>
        public override void UpdateWorldLights(bool recurse) {
            _worldLights.Clear();

            LightCombineHint hint = base.SceneHints.LightCombineHint;

            if(hint == LightCombineHint.Local) {
                _worldLights.Set(base.Lights);
                _worldLights.SortLights(this);
            } else if(hint == LightCombineHint.CombineClosest){
                //Ensure we're up to date with the local list's other parameters
                _worldLights.Set(base.Lights, false);

                //Collect all lights that can influence us, then sort
                CollectLights(_worldLights);
                _worldLights.SortLights(this);
            }

            ClearDirty(DirtyMark.Lighting);
        }

        /// <summary>
        /// Sets a single solid vertex color in the Mesh's MeshData, matching
        /// the number of vertices contained. If none are present, the method will return.
        /// If the geometry already has colors, it will update the vertex buffer and not reconstruct it.
        /// </summary>
        /// <param name="color">Color to set</param>
        public void SetSolidColor(Color color) {
            //Abort if no mesh data
            if(_meshData == null) {
                return;
            }

            //Get the position data - we'll use this to match the # of colors.
            DataBuffer<Vector3> pos = _meshData.Positions;
            if(pos == null) {
                return;
            }

            //Check to see if we already have colors
            DataBuffer<Color> colors = _meshData.Colors;
            if(colors == null) {
                colors = new DataBuffer<Color>(pos.Length);
            }

            //Set the data
            colors.Position = 0;
            while(colors.HasNext) {
                colors.Set(color);
            }

            //Update the vertex color info - this either will update the VB or add it to he buffer map and 
            //flag the mesh data as dirty
            _meshData.UpdateVertexData<Color>(VertexSemantic.Color, colors);

            //If our VB needs to be reconstructed, do so
            if(_meshData.VertexBufferDirty) {
                _meshData.Reconstruct();
            }
        }

        /// <summary>
        /// Sets random colors with constant alpha in the geometry, matching the number of vertices.
        /// If none are present, the method will return. If the geometry already has colors, it will 
        /// update the vertex buffer and not reconstruct it, otherwise it will rebuild the vertex buffer.
        /// </summary>
        /// <param name="alpha">Alpha value to use</param>
        public void SetRandomColors(float alpha) {
            //Abort if no mesh data
            if(_meshData == null) {
                return;
            }

            //Get the position data - we'll use this to match the # of colors.
            DataBuffer<Vector3> pos = _meshData.Positions;
            if(pos == null) {
                return;
            }

            //Random value stream from the engine
            Random rand = Engine.ValueMap.Random;

            //Check to see if we already have colors
            DataBuffer<Color> colors = _meshData.Colors;
            if(colors == null) {
                colors = new DataBuffer<Color>(pos.Length);
            }

            //Set the data
            colors.Position = 0;
            while(colors.HasNext) {
                colors.Set(new Color((float) rand.NextDouble(), (float) rand.NextDouble(), (float) rand.NextDouble(), alpha));
            }

            //Update the vertex color info - this either will update the VB or add it to he buffer map and 
            //flag the mesh data as dirty
            _meshData.UpdateVertexData<Color>(VertexSemantic.Color, colors);

            //If our VB needs to be reconstructed, do so
            if(_meshData.VertexBufferDirty) {
                _meshData.Reconstruct();
            }
        }

        /// <summary>
        /// Sets random colors in the geometry, matching the number of vertices.
        /// If none are present, the method will return. If the geometry already has colors, it will 
        /// update the vertex buffer and not reconstruct it.
        /// </summary>
        public void SetRandomColors() {
            SetRandomColors(1.0f);
        }

        /// <summary>
        /// Compute the tangent basis of the mesh's geometry. This requires the geometry
        /// to have both normals and texture coordinates (first index is used). It will reconstruct the
        /// vertex buffer.
        /// </summary>
        public void ComputeTangentBasis() {
            if(_meshData == null || _meshData.PrimitiveType == PrimitiveType.LineList
                || _meshData.PrimitiveType == PrimitiveType.LineStrip) {
                return;
            }

            DataBuffer<Vector2> texCoords = _meshData.TextureCoordinates;
            DataBuffer<Vector3> normals = _meshData.Normals;
            DataBuffer<Vector3> pos = _meshData.Positions;

            if(texCoords == null || normals == null || pos == null) {
                return;
            }

            DataBuffer<Vector3> binormals = _meshData.Binormals;
            DataBuffer<Vector3> tangents = _meshData.Tangents;

            if(binormals == null) {
                binormals = new DataBuffer<Vector3>(pos.Length);
            }

            if(tangents == null) {
                tangents = new DataBuffer<Vector3>(pos.Length);
            }

            binormals.Position = 0;
            tangents.Position = 0;
            _meshData.UpdateVertexCount();
            int[] triVerts = new int[3];
            for(int i = 0; i < _meshData.PrimitiveCount; i++) {
                _meshData.GetPrimitiveIndices(i, triVerts);
                int index0 =  triVerts[0];
                int index1 =  triVerts[1];
                int index2 =  triVerts[2];
                //Get the triangle verts
                Vector3 p0 = pos.Get(index0);
                Vector3 p1 = pos.Get(index1);
                Vector3 p2 = pos.Get(index2);
                //Get the triangle uv coords
                Vector2 uv0 = texCoords.Get(index0);
                Vector2 uv1 = texCoords.Get(index1);
                Vector2 uv2 = texCoords.Get(index2);

                float x1 = p1.X - p0.X;
                float x2 = p2.X - p0.X;
                float y1 = p1.Y - p0.Y;
                float y2 = p2.Y - p0.Y;
                float z1 = p1.Z - p0.Z;
                float z2 = p2.Z - p0.Z;

                float s1 = uv1.X - uv0.X;
                float s2 = uv2.X - uv0.X;
                float t1 = uv1.Y - uv0.Y;
                float t2 = uv2.Y - uv0.Y;

                //calculate determinate
                float r = 1.0f / (s1 * t2 - s2 * t1);
                //Calculate the s, t directions
                Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,
                        (t2 * z1 - t1 * z2) * r);
                Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1)* r,
                        (s1 * z2 - s2 * z1) * r);

                //Add them to the existing tangent, binormal arrays
                tangents.Set(tangents.Get(index0) + sdir, index0);
                tangents.Set(tangents.Get(index1) + sdir, index1);
                tangents.Set(tangents.Get(index2) + sdir, index2);

                binormals.Set(binormals.Get(index0) + tdir, index0);
                binormals.Set(binormals.Get(index1) + tdir, index1);
                binormals.Set(binormals.Get(index2) + tdir, index2);
            }

            //Gram-Schmidt orthogonalization
            for(int i = 0; i < normals.Length; i++) {
                Vector3 n = normals.Get(i);
                Vector3 t = tangents.Get(i);
                Vector3 b = binormals.Get(i);

                Vector3 tangent = Vector3.Normalize(t - n * Vector3.Dot(n, t));

                float handy = (Vector3.Dot(Vector3.Cross(n, t), b) < 0) ? -1.0f : 1.0f;
                b = Vector3.Cross(n, t);
                b *= handy;
                tangents.Set(tangent, i);
                binormals.Set(Vector3.Normalize(b), i);
            }

            _meshData.UpdateVertexData<Vector3>(VertexSemantic.Binormal, binormals);
            _meshData.UpdateVertexData<Vector3>(VertexSemantic.Tangent, tangents);

            if(_meshData.VertexBufferDirty) {
                _meshData.Reconstruct();
            }
        }

        /// <summary>
        /// Performs a Ray-Triangle intersection test on the geometry. Will return
        /// an empty record for line meshes.
        /// </summary>
        /// <param name="ray">Ray to test with</param>
        /// <returns>Primitive intersection record</returns>
        public PrimitiveIntersectionRecord IntersectsPrimitivesWhere(Ray ray) {
            PrimitiveType primType = _meshData.PrimitiveType;
            //For now avoid lines
            if(primType == PrimitiveType.LineList || primType == PrimitiveType.LineStrip) {
                return new PrimitiveIntersectionRecord();
            }

            List<IntersectionRecord> records = new List<IntersectionRecord>();
            Vector3[] tri = new Vector3[3];
            Transform worldTransform = base.WorldTransform;
            for(int i = 0; i < _meshData.PrimitiveCount; i++) {
                _meshData.GetPrimitive(i, tri);
                Vector3 p0 = tri[0];
                Vector3 p1 = tri[1];
                Vector3 p2 = tri[2];

                worldTransform.TransformVector(ref p0, out p0);
                worldTransform.TransformVector(ref p1, out p1);
                worldTransform.TransformVector(ref p2, out p2);

                Triangle triangle = new Triangle(p0, p1, p2);
                IntersectionRecord? record = Ray.Intersects(ref ray, ref triangle);
                if(record.HasValue) {
                    records.Add(record.Value);
                }
            }

            if(records.Count > 0) {
                IntersectionRecord[] temp = new IntersectionRecord[records.Count];
                for(int i = 0; i < records.Count; i++) {
                    temp[i] = records[i];
                }

                return new PrimitiveIntersectionRecord(temp);
            } else {
                return new PrimitiveIntersectionRecord();
            }
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public override void Write(ISavableWriter output) {
            base.Write(output);
            output.WriteExternal<Material>("Material", _material);
            output.WriteSavable<MeshData>("MeshData", _meshData);
            output.WriteSavable<BoundingVolume>("ModelBound", _modelBound);
        }

        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public override void Read(ISavableReader input) {
            base.Read(input);
          //  _material = input.ReadExternal<Material>();
            _meshData = input.ReadSavable<MeshData>();
            _modelBound = input.ReadSavable<BoundingVolume>();
            _worldLights = new LightCollection();
            SetModelBound(_modelBound);
        }
    }
}
